package org.bgee.psp4jdbc.pool;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentNavigableMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bgee.psp4jdbc.Properties;

/**
 * The aim of this class is to count every pooled elements in every <code>Pool</code>s
 * existing in the application, and to decide which pool has to be cleaned when the global
 * maximum capacity is reached.
 * <p>
 * Every instance of <code>Pool</code> automatically report its state to this class.
 * <p>
 * The overall maximum capacity is defined in 
 * {@link org.bgee.psp4jdbc.Properties#getGlobalMaxSize()}
 * <p>
 * This class is meant to be used in a static way and only by the class <code>Pool</code>.
 * 
 * @author Frederic Bastian
 * @author Mathieu Seppey
 * @version 1, August 2013
 * @since 1
 *
 */
public class PoolMonitor
{

    /**
     * <code>Logger</code> of the class. 
     */
    private final static Logger log = LogManager.getLogger(PoolMonitor.class.getName());

    /**
     * A <code>ConcurrentNavigableMap</code> that contains all known <code>Pool</code>s, 
     * ordered by their size. A key in this <code>Map</code> can be associated to 
     * several <code>Pool</code>s of same size.
     * This <code>Map</code> is a concurrent <code>Map</code>, and the values 
     * are non-concurrent <code>Set</code>s (but a lock will be obtained 
     * on these <code>Set</code>s before any read/write). 
     */
    private final static ConcurrentNavigableMap<Integer, Set<Pool>> poolsBySize = 
    		new ConcurrentSkipListMap<Integer, Set<Pool>>();
    
    /**
     * A concurrent <code>Set</code> (backed up by a <code>ConcurrentHashMap</code>) 
     * to store all <code>Pool</code>s registered (more convenient for the method 
     * {@link #close()}).
     */
    private final static Set<Pool> pools = 
    		Collections.newSetFromMap(new ConcurrentHashMap<Pool, Boolean>());
    
    /**
     * An <code>AtomicInteger</code> that contains the total number of pooled elements 
     */
    private static final AtomicInteger totalPoolSize = new AtomicInteger(0);

    /**
     * private constructor to prevent instantiation of this class
     */
    private PoolMonitor()
    {
    }
 
    /**
     * Method called by a <code>Pool</code> to inform that the number 
     * of <code>PoolElement</code>s it holds has changed. 
     * This method then verifies whether the maximum number of <code>PoolElement</code>s
     * over all <code>Pool</code>s has been reached, and triggers a cleaning 
     * if it is the case.
     *
     * @param pool			The caller <code>Pool</code>, reporting a change in its status.
     * @param previousSize 	an <code>int</code> representing the size of the caller 
     * 						<code>Pool</code> before the change of size.
     * @param sizeChange	an <code>int</code> representing the change of size 
     * 						of <code>pool</code>. This is needed because the size 
     * 						of <code>pool</code> might not have actually changed yet 
     * 						(for instance, this method is called <strong>before</strong> 
     * 						adding an element to a <code>Pool</code>, to make sure 
     * 						the overall maximum capacity is not exceeded)
     * @throws IllegalStateException	If <code>previousSize</code> did not allow 
     * 									to retrieve <code>pool</code> in this monitor.
     * @throws ReleaseException 	If an error occurred while trying to release 
     * 								a <code>PoolElement</code> from a <code>Pool</code>, 
     * 								if the overall maximum capacity was reached.
     */
	protected static void updatePoolSize(Pool pool, int previousSize, int sizeChange) 
    		throws IllegalStateException, ReleaseException {
		System.out.println("updatePoolSize: " + pool + " - " + previousSize + " - " + sizeChange);
        log.entry(pool, previousSize, sizeChange);
        if (sizeChange == 0) {
        	log.exit(); return;
        }
        
        // First, remove the Pool from the position it held in the Map
        if (previousSize != 0) {
        	try {
        		PoolMonitor.removePool(previousSize, pool);
        	} catch (IllegalStateException e) {
        		pool.close();
        		throw log.throwing(new IllegalStateException(
        				"A pool reported an incorrect size and could not be located. " +
        				"Pool causing the error reported a previous size of " + 
        				previousSize + ", an addition of " + sizeChange + 
        				" elements, and has a current size of " + pool.size()));
        	}
        }

        //add the pool to its new position if needed
        int newSize = previousSize + sizeChange;
        if (newSize != 0) {
    		//if the pool was not already registered to this PoolMonitor
        	if (previousSize == 0) {
        		pools.add(pool);
        	}
        	
        	// Then, add the Pool to poolsBySize
        	// If another Pool occupies the same position, a Collection already exists.
        	Set<Pool> newPools = new HashSet<Pool>();
        	newPools.add(pool);
        	while (true){
        		Collection<Pool> previousPools = poolsBySize.putIfAbsent(newSize, newPools);
        		if (previousPools == null) {
        			break;
        		}
        		//synchronized block necessary to make sure it is not removed from 
        		//poolsBySize before we add pool to it.
        		synchronized(previousPools) {
        			if (previousPools.isEmpty()) {
        				//if the previousPools is empty, it means it was removed 
        				//(or about to be removed) from poolsBySize 
        				//before we had a chance to add pool to it. 
        				//So, let's try again. 
        				//(actually, previousPools could be emptied before being removed 
        				//from poolsBySize, but a lock will be obtained on it to check atomically 
        				//that it is empty before removing it, so it is equivalent; 
        				//worst case scenario we will have a few loops to wait for previousPools 
        				//to be actually removed)
        				continue;
        			}
        			//otherwise, thanks to the synchronized block, we are sure 
        			//that another thread could not empty and remove it from poolsBySize now
        			previousPools.add(pool);
        			break;
        		}
        	} 
        } else if (previousSize != 0) {
        	//otherwise, the Pool should not be registered to this PoolMonitor anymore
        	pools.remove(pool);
        }

        // Check whether the maximum allowed is reach and start a cleaning if needed 
        totalPoolSize.addAndGet(sizeChange);
        log.debug("Total number of pooled elements is {}", totalPoolSize);
        //clean pools only if an element was added
        if (sizeChange > 0) {
            cleanPools();
        }
        
        log.exit();        
    }
    
    /**
     * Remove <code>pool</code> from this <code>PoolMonitor</code>.
     * 
     * @param size 	An <code>int</code> corresponding to the size used to stored 
     * 				<code>pool</code> in this <code>PoolMonitor</code>. Might be different 
     * 				then the value returned by {@link Pool#size()}.
     * @param pool 	The <code>Pool</code> to remove.
     * @throws IllegalStateException	If <code>size</code> did not allow 
     * 									to retrieve <code>pool</code> in this monitor.
     */
    public static void removePool(int size, Pool pool) throws IllegalStateException {
    	log.entry(size, pool);
    	
    	if (size != 0) {
        	Collection<Pool> poolsOldSize = poolsBySize.get(size);
        	if (poolsOldSize != null && poolsOldSize.remove(pool)) {
        		//if no more Pools are associated to that size, 
        		//remove the Collection.
        		//synchronized on this Collection so that no element can be added to it 
        		//before we remove it from poolsBySize
        		if(poolsOldSize.isEmpty()) {
        			synchronized(poolsOldSize) {
        				if(poolsOldSize.isEmpty()){
        					poolsBySize.remove(size);
        				}
        			}
        		}
        	} else {
        		throw log.throwing(new IllegalStateException(
        				"A pool reported an incorrect size and could not be located. " +
        				"Pool causing the error reported a previous size of " + size));
        	}
        }
    	log.exit();
    }

    /**
     * This method retrieves every <code>Pool</code>s that occupy the top position
     * related to their size, and ask to some of them to drop a <code>PoolElement</code> 
     * by calling <code>Pool#removeLRU()</code>, in order to get the global pool size 
     * below the overall maximum capacity.
     * 
     * @throws ReleaseException 	If an error occurred while trying to release 
     * 								a <code>PoolElement</code> from a <code>Pool</code>.
     */
    private static void cleanPools() throws ReleaseException {
    	log.entry();

        if (totalPoolSize.get() <= Properties.getGlobalMaxSize()) {
    		log.exit(); return;
    	}
        log.debug("Global size of {} over {} allowed, cleaning pools {}.", 
        		totalPoolSize.get(), Properties.getGlobalMaxSize(), poolsBySize);
        //as long as we don't get below the maximum size, we clean pools
        while (true) {
    		if (totalPoolSize.get() <= Properties.getGlobalMaxSize()) {
    			log.trace("Cleaning done, {}", poolsBySize);
    			log.exit(); return;
    		}
    		Entry<Integer, Set<Pool>> poolEntry = poolsBySize.lastEntry();
    		if (poolEntry != null) {
    			try {
    			    Collection<Pool> poolsToClean = poolEntry.getValue();
    			    log.trace("Pools with the higher number of elements: {}", poolsToClean);
    				Pool pool = poolsToClean.iterator().next();
    				log.trace("Trigerring eviction on {}", pool);
    				pool.triggerEviction();
    			} catch (IllegalStateException e) {
    				//poolsToClean was emptied or removed by another thread, try again
    				continue;
    			} catch (NoSuchElementException e) {
    				//poolsToClean was emptied or removed by another thread, try again
    				continue;
    			}
    		}
        }
    }
    
    /**
     * Call {@link Pool#close()} on all <code>Pools</code> registered to this monitor.
     * 
     * @throws ReleaseException 	If an error occurred while releasing 
     * 								a <code>Pool</code>. 
     */
    public static void close() throws ReleaseException {
    	log.entry();
    	log.debug("Closing all pools");
    	//to avoid ConcurrentModificationException
    	Collection<Pool> shallowCopy = 
    			new ArrayList<Pool>(pools);
    	for (Pool pool: shallowCopy) {
    		pool.close();
    	}
    	poolsBySize.clear();
    	pools.clear();
    	totalPoolSize.set(0);
    	log.exit();
    }
    
    /**
     * @return	an <code>int</code> representing the current number of 
     * 			<code>PoolElement</code>s stored in all <code>Pool</code>s.
     */
    public static int getTotalPoolSize() {
    	return totalPoolSize.get();
    }

}